<?php

/**
 * Ignus Framework
 *
 * @author Sergey Palyanov <sergey.palyanov@gmail.com>
 * @link http://code.google.com/p/ignus-php/
 * @license http://apache.org/licenses/LICENSE-2.0
 * @package Ignus
 * @subpackage Mock
 * @since 0.1
 */

namespace Ignus\Test\Mock;

/**
 * @package Ignus
 * @subpackage Mock
 * @since 0.1
 */
class Generator
{
    /**
     * Cached generated mock classes
     * @var array
     */
    private static $cached = array();

    /**
     * @var ReflectionClass
     */
    private $class;

    /**
     * Reflect source class and generate mocked class name
     * @throws RuntimeException If source class is final, we cannot mock it
     * @param string $class Source class name
     */
    private function __construct($class)
    {
        $this->class = new \ReflectionClass($class);

        // check if it's not a final class
        if ($this->class->isFinal())
            throw new \RuntimeException("Can't create mock: $class is final");
    }

    /**
     * Generates class name with public interface cloned from source class
     * @todo Create and use code generation package
     * @return string Newly generated mock class name
     */
    private function generate()
    {
        $mockedClassName = 'Mocked' . $this->class->getShortName() . uniqid();

        $code = 'class ' . $mockedClassName .
            ($this->class->isInterface() ? ' implements ' : ' extends ') .
            $this->class->getName() . '{';

        $code .= 'private $_invokator = null;'
            . 'private $_stubs = array();';

        $methods = $this->class->getMethods(\ReflectionMethod::IS_PUBLIC);
        foreach ($methods as $method) {
            if (!self::isMethodMockable($method))
                continue;

            $code .= 'public function ' . $method->getName() . '('
                . $this->generateParams($method). '){$args=func_get_args();'
                . '$this->_invoke("'. $method->getName() . '",$args);'
                . 'return $this->_stubbed("'.$method->getName().'", $args);}';
        }

        $code .= 'public function expects($times=1){'
            . 'if($this->_invokator == null)$this->_invokator='
            . 'new \Ignus\Test\Mock\Invokator();'
            . 'return new \Ignus\Test\Mock\Expectation($this->_invokator, $times);}';

        $code .= 'public function stub(){'
            . 'return new \Ignus\Test\Mock\Stub($this);}';

        $code .= 'public function registerStub($methodName,array $arguments,$value){'
            . '$this->_stubs[\Ignus\Test\Mock\Invokator::hash($methodName, $arguments)] = $value;}';

        $code .= 'private function _invoke($name, array $args){'
            . 'if($this->_invokator == null)$this->_invokator='
            . 'new \Ignus\Test\Mock\Invokator();'
            . '$this->_invokator->invoke($name,$args);}';

        $code .= 'private function _stubbed($name, array $args){if(!count($this->_stubs))'
            . 'return null;$key = \Ignus\Test\Mock\Invokator::hash($name, $args);'
            . 'return isset($this->_stubs[$key])?$this->_stubs[$key]:null;}';

        $code .= 'public function __destruct(){'
            . 'if($this->_invokator != null)$this->_invokator->check();}';

        eval($code . "}");

        return $mockedClassName;
    }

    /**
     * Returns string of declared method arguments
     * @param ReflectionMethod $method
     * @return string
     */
    private function generateParams(\ReflectionMethod $method)
    {
        if ($method->getNumberOfParameters() == 0)
            return '';

        $parameters = array();
        foreach ($method->getParameters() as $param) {
            $parameters[] = $this->generateParam($param);
        }

        return implode(', ', $parameters);
    }

    /**
     * Generate method argument declaration
     * @param ReflectionParameter $param
     * @return string Argument declaration
     */
    private function generateParam(\ReflectionParameter $param)
    {
        $var = '$'. $param->getName();

        if ($param->isPassedByReference())
            $var = '&' . $var;

        if ($param->isDefaultValueAvailable())
            $var .= '=' . var_export($param->getDefaultValue(), true);

        if ($param->isArray())
            return 'array ' . $var;
        else if ($class = $param->getClass())
            return $class->getName() . ' ' . $var;

        return $var;
    }

    /**
     * Helper method to generate mock object
     * @param string $class Class name to mock
     * @param array $args Arguments to pass into constructor
     * @return object Instance of mock class
     */
    public static function createMock($class, array $args = null)
    {
        if (!isset(self::$cached[$class])) {
            $generator = new Generator($class);
            self::$cached[$class] = $generator->generate();
        }

        if ($args == null)
            return new self::$cached[$class]();

        $reflected = new \ReflectionClass(self::$cached[$class]);
        return $reflected->newInstanceArgs($args);
    }

    /**
     * @param ReflectionMethod $method Method to check
     * @return bool True if method can be mocked, false otherwise
     */
    public static function isMethodMockable(\ReflectionMethod $method)
    {
        return !($method->isFinal() || $method->isConstructor());
    }
}
